//
//  PLDirectory.m
//  Plankton
//
//  Created by Marc Schlichte on 2008-07-13.
//  Copyright 2008 Framework Labs. All rights reserved.
//

#import "PLDirectory.h"

#import "PLPlatform.h"

#define PLAP_TYPE @"_plap._udp"


@interface PLDirectory ()

- (void)notePeersChanged;
- (void)maybePush:(id)message aid:(NSString *)aid service:(NSString *)service;

@end

@implementation PLDirectory

@synthesize delegate, aid = dirAid;
@synthesize discoverable, autoDiscover;
@synthesize receiveUpdates;

- (id)initWithPlatform:(PLPlatform *)platform_
{
    if (self = [super init]) {
        platform = [platform_ retain];
        dirAid = [[platform makeAid] retain];
        
        serviceToAidsDict = [[NSMutableDictionary alloc] init];
        peers = [[NSMutableSet alloc] init];
        
        [self registerAid:dirAid forService:@"plankton.directory"];
        
        aidsResponder = [[PLResponder alloc] initWithContext:self];
        [aidsResponder serveRequestsNamed:@"queryAids" syncHandler:@selector(_onQueryAidsRequest:)];
        
        servicesResponder = [[PLResponder alloc] initWithContext:self];
        [servicesResponder serveRequestsNamed:@"queryServices" syncHandler:@selector(_onQueryServicesRequest:)];
        
        ownService = [[NSNetService alloc] initWithDomain:@"" type:PLAP_TYPE name:dirAid port:[PLPlatform portOfAid:dirAid]];
        [ownService setDelegate:self];
        
        serviceBrowser = [[NSNetServiceBrowser alloc] init];
        [serviceBrowser setDelegate:self];
        
        peersToPush = [[NSMutableSet alloc] init];
        
        pushRegistrationPattern = [[PLPattern patternWithPredicateFormat:@"SELF[FIRST] in {'requestPushes', 'unrequestPushes'}"] 
                                   retain];
        [pushRegistrationPattern setMessageTarget:self action:@selector(onPushRegistration:pattern:)];
        [platform receive:pushRegistrationPattern by:dirAid];
        
        pushPattern = [[PLPattern patternWithPredicateFormat:@"SELF[FIRST] in {'onAdded', 'onRemoved' }"]
                       retain];
        [pushPattern setMessageTarget:self action:@selector(onPush:pattern:)];
        [platform receive:pushPattern by:dirAid];
    }
    return self;
}

- (void)dealloc
{   
    self.receiveUpdates = NO;
    [peersToPush release];
    [pushRegistrationPattern release];
    [pushPattern release];
    
    [serviceBrowser release];
    [ownService release];
    
    [servicesRequestor release];
    [foundServices release];
    [servicesResponder release];
    
    [aidsRequestor release];
    [serviceToSearch release];
    [aidsResponder release];
    
    [peers release];
    [serviceToAidsDict release];
    [dirAid release];
    [platform release];
    
    [super dealloc];
}

- (void)registerAid:(NSString *)aid forService:(NSString *)service
{
    NSMutableArray *aids = [serviceToAidsDict objectForKey:service];
    if (!aids) {
        aids = [NSMutableArray array];
        [serviceToAidsDict setObject:aids forKey:service];
    }
    [aids addObject:aid];
    [self maybePush:@"onAdded" aid:aid service:service];
}

- (void)unregisterAid:(NSString *)aid
{
    for (NSString *service in serviceToAidsDict) {
        NSMutableArray *aids = [serviceToAidsDict objectForKey:service];
        NSUInteger index = [aids indexOfObject:aid];
        if (index != NSNotFound) {
            [aids removeObject:aid];
            [self maybePush:@"onRemoved" aid:aid service:service];
            return;
        }
    }
}

- (void)searchAidsForService:(NSString *)service
{
	NSParameterAssert(service);
	
    // don't search if already searching
    if ([self isSearchingForAids]) {
        return;
    }
    
    // delegate must be able to listen, otherwise don't bother doing any work
    if (![delegate respondsToSelector:@selector(directory:foundAid:forService:)]) {
        if ([delegate respondsToSelector:@selector(directory:searchDoneForService:)]) {
            [delegate directory:self searchDoneForService:service];
        }
        return;
    }
        
    // ok, look locally
    for (NSString *aid in [serviceToAidsDict objectForKey:service]) {
       [delegate directory:self foundAid:aid forService:service];
    }
    
    // now, query all peers for their infos - duplicates are not filtered
    serviceToSearch = [service copy];
    aidsRequestor = [[PLMulticastRequestor alloc] initWithContext:self];
    [aidsRequestor setTimeoutHandler:@selector(_onQueryAidsTimeout:requestor:) after:10.0];
    [aidsRequestor multicastRequestNamed:@"queryAids" 
                               arguments:service
                                      to:peers 
                         responseHandler:@selector(_onQueryAidsResponse:requestor:)
                             joinHandler:@selector(_onQueryAidsDone:)];
}

- (void)searchServices
{
    // only one request allowed at a time
    if (servicesRequestor) {
        return;
    }
    
    // don't bother calculating stuff if we don't have someone listening
    if (![delegate respondsToSelector:@selector(directory:foundServices:)]) {
        return;
    }

    foundServices = [[NSMutableSet alloc] initWithArray:[serviceToAidsDict allKeys]];
    servicesRequestor = [[PLMulticastRequestor alloc] initWithContext:self];
	[servicesRequestor setTimeoutHandler:@selector(_onQueryServicesTimeout:requestor:) after:10.0];
    [servicesRequestor multicastRequestNamed:@"queryServices"
                                   arguments:nil
                                          to:peers
                             responseHandler:@selector(_onQueryServicesResponse:requestor:)
                                 joinHandler:@selector(_onQueryServicesDone:)];
}

- (BOOL)isSearchingForAids
{
    return aidsRequestor != nil;
}

- (void)addPeer:(NSString *)aid
{
    if (![peers containsObject:aid] && ![aid isEqualToString:dirAid]) {
        [peers addObject:aid];
		
		// inform peer that we want push updates from it
		if (receiveUpdates) {
			[platform send:[NSArray arrayWithObjects:@"requestPushes", dirAid, nil] to:aid];
		}
		
        [self notePeersChanged];
    }
}

- (void)removePeer:(NSString *)aid
{
    if ([peers containsObject:aid]) {
        [peers removeObject:aid];
		
		// inform peer that we don't want push updates anymore
		if (receiveUpdates) {
			[platform send:[NSArray arrayWithObjects:@"unrequestPushes", dirAid, nil] to:aid];
		}
			
        [self notePeersChanged];
    }
}

#pragma mark interactor context impls

- (PLPlatform *)platformForInteractor:(PLInteractor *)interactor
{
    return platform;
}

- (NSString *)aidForInteractor:(PLInteractor *)interactor
{
    return dirAid;
}

#pragma mark searchAidsForService section

- (id)_onQueryAidsRequest:(id)service
{
    return [serviceToAidsDict objectForKey:service];
}

- (void)_onQueryAidsResponse:(id)aids requestor:(PLRequestor *)requestor
{
    for (NSString *aid in aids) {
        [delegate directory:self foundAid:aid forService:serviceToSearch];
    }
}

- (void)_onQueryAidsTimeout:(NSString *)aid requestor:(PLRequestor *)requestor 
{
	NSLog(@"timeout of %@ on queryAids", aid);
}

- (void)_onQueryAidsDone:(PLRequestor *)requestor
{
    NSString *service = serviceToSearch;

    // set this to nil before delegate calls so that delegate can search again
    [serviceToSearch autorelease], serviceToSearch = nil;
    [aidsRequestor autorelease], aidsRequestor = nil;;

    if ([delegate respondsToSelector:@selector(directory:searchDoneForService:)]) {
        [delegate directory:self searchDoneForService:service];
    }    
}

#pragma mark search services section

- (id)_onQueryServicesRequest:(id)empty
{
    return [serviceToAidsDict allKeys];
}

- (void)_onQueryServicesResponse:(id)services requestor:(PLRequestor *)requestor
{
    [foundServices unionSet:services];
}

- (void)_onQueryServicesTimeout:(NSString *)aid requestor:(PLRequestor *)requestor 
{
	NSLog(@"timeout of %@ on queryServices", aid);
}

- (void)_onQueryServicesDone:(PLRequestor *)requestor
{
    NSSet *services = foundServices;
    [foundServices autorelease], foundServices = nil;
    [servicesRequestor autorelease], servicesRequestor = nil;
    
    if ([delegate respondsToSelector:@selector(directory:foundServices:)]) {
        [delegate directory:self foundServices:services];
    }
}

#pragma mark internal stuff

- (void)notePeersChanged
{
    if ([delegate respondsToSelector:@selector(directoryChangedPeerSet:)]) {
        [delegate directoryChangedPeerSet:self];
    }
}

- (void)maybePush:(id)perf aid:(NSString *)aid service:(NSString *)service;
{
    if ([peersToPush count] > 0) {
        NSArray *message = [NSArray arrayWithObjects:perf, aid, service, nil];
        [platform multicast:message to:peersToPush];
    }
}

#pragma mark auto discover section

- (void)setDiscoverable:(BOOL)val
{
    if (discoverable != val) {
        if (discoverable) {
            [ownService stop];
        }
        discoverable = val;
        if (discoverable) {
            [ownService publish];
        }
    }
}

- (void)setAutoDiscover:(BOOL)val
{
    if (autoDiscover != val) {
        if (autoDiscover) {
            [serviceBrowser stop];
        }
        autoDiscover = val;
        if (autoDiscover) {
            [serviceBrowser searchForServicesOfType:PLAP_TYPE inDomain:@""];
        }
    }
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
           didFindService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing
{
    [self addPeer:[aNetService name]];
}

- (void)netServiceBrowser:(NSNetServiceBrowser *)browser
         didRemoveService:(NSNetService *)aNetService
               moreComing:(BOOL)moreComing
{
    [self removePeer:[aNetService name]];
}

#pragma mark push section

- (void)setReceiveUpdates:(BOOL)val
{
    if (receiveUpdates != val) {
        receiveUpdates = val;
		
		NSMutableSet *peersAndSelf = [peers mutableCopy];
		[peersAndSelf addObject:dirAid];
		
        [platform multicast:[NSArray arrayWithObjects:(val ? @"requestPushes" : @"unrequestPushes"), dirAid, nil] 
                         to:peersAndSelf];
		[peersAndSelf release];
    }
}

- (void)onPushRegistration:(id)message pattern:(PLPattern *)pat
{
    NSString *perf = [message objectAtIndex:0];
    NSString *aid = [message objectAtIndex:1];
    if ([perf isEqual:@"requestPushes"]) {
        [peersToPush addObject:aid];
    }
    else {
        [peersToPush removeObject:aid];
    }
    [platform receive:pushRegistrationPattern by:dirAid];
}

- (void)onPush:(id)message pattern:(PLPattern *)pat
{
    NSString *perf = [message objectAtIndex:0];
    if ([perf isEqual:@"onAdded"] && [delegate respondsToSelector:@selector(directory:addedAid:forService:)]) {
        [delegate directory:self addedAid:[message objectAtIndex:1] forService:[message objectAtIndex:2]];
    }
    else if ([perf isEqual:@"onRemoved"] && [delegate respondsToSelector:@selector(directory:removedAid:forService:)]) {
        [delegate directory:self removedAid:[message objectAtIndex:1] forService:[message objectAtIndex:2]];
    }
    [platform receive:pushPattern by:dirAid];
}

@end
